<?php
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK IT ]
// +----------------------------------------------------------------------
// | Copyright (c) 2011 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: luofei614 <weibo.com/luofei614>
// | 修改者: zhtp 原是thinkphp3.2.3 自带库 现基于tp8.0重写+简化
// +----------------------------------------------------------------------
namespace zhtp;

use think\facade\Db;
use think\facade\Cache;

/**
 * 权限认证类
 * 功能特性：
 * 1，是对规则进行认证，不是对节点进行认证。用户可以把节点当作规则名称实现对节点进行认证。
 *      $auth=new Auth();  $auth->check('规则名称','用户id')
 * 2，可以同时对多条规则进行认证，并设置多条规则的关系（or或者and）
 *      $auth=new Auth();  $auth->check('规则1,规则2','用户id','and')
 *      第三个参数为and时表示，用户需要同时具有规则1和规则2的权限。 当第三个参数为or时，表示用户值需要具备其中一个条件即可。默认为or
 * 3，一个用户可以属于多个用户组(think_auth_group_access表 定义了用户所属用户组)。我们需要设置每个用户组拥有哪些规则(think_auth_group 定义了用户组权限)
 *
 * 4，支持规则表达式。
 *      在think_auth_rule 表中定义一条规则时，如果type为1， [-]condition字段就可以定义规则表达式。 如定义{score}>5  and {score}<100  表示用户的分数在5-100之间时这条规则才会通过。
 */

//数据库
/*
-- ----------------------------
-- think_auth_rule，规则表，
-- id:主键，name：规则唯一标识, title：规则中文名称 status 状态：为1正常，为0禁用，condition：规则表达式，为空表示存在就验证，不为空表示按照条件验证
-- ----------------------------
DROP TABLE IF EXISTS `think_auth_rule`;
CREATE TABLE `think_auth_rule` (
`id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
`name` char(80) NOT NULL DEFAULT '',
`title` char(20) NOT NULL DEFAULT '',
`type` tinyint(1) NOT NULL DEFAULT '1',
`status` tinyint(1) NOT NULL DEFAULT '1',
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;
-- ----------------------------
-- think_auth_group 用户组表，
-- id：主键， title:用户组中文名称， rules：用户组拥有的规则id， 多个规则","隔开，status 状态：为1正常，为0禁用
-- ----------------------------
DROP TABLE IF EXISTS `think_auth_group`;
CREATE TABLE `think_auth_group` (
`id` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
`parent_id` int DEFAULT NULL,
`parent_ids` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '上级ids',
`sort` tinyint unsigned DEFAULT '99' COMMENT '排序',
`title` char(100) NOT NULL DEFAULT '',
`status` tinyint(1) NOT NULL DEFAULT '1',
`rules` text NOT NULL DEFAULT '',
`admin_id` int DEFAULT NULL COMMENT '管理员id',
`dept_id` int DEFAULT NULL COMMENT '部门id',
PRIMARY KEY (`id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8;
-- ----------------------------
-- think_auth_group_access 用户组明细表
-- uid:用户id，group_id：用户组id
-- ----------------------------
DROP TABLE IF EXISTS `think_auth_group_access`;
CREATE TABLE `think_auth_group_access` (
`uid` mediumint(8) unsigned NOT NULL,
`group_id` mediumint(8) unsigned NOT NULL,
UNIQUE KEY `uid_group_id` (`uid`,`group_id`),
KEY `uid` (`uid`),
KEY `group_id` (`group_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
 */

class Auth
{

    //默认配置
    protected $_config = array(
        'AUTH_ON' => true, // 认证开关
        'AUTH_TYPE' => 1, // 认证方式，1为实时认证；2为登录认证。
        'AUTH_GROUP' => 'admin_auth_group', // 用户组数据表名
        'AUTH_GROUP_ACCESS' => 'admin_auth_group_access', // 用户-用户组关系表
        'AUTH_RULE' => 'admin_auth_rule', // 权限规则表
        'AUTH_USER' => 'admin', // 用户信息表
        'AUTH_DEPT' => 'dept', // 部门表

        'CACHE_ON' => false,
        'CACHE_EXPIRE' => 7200, // 缓存时间
        'CACHE_AUTH_GROUP_WITH_UID' => 'ADMIN_AUTH_GROUP_WITH_UID',
        'CACHE_AUTH_GROUP_TAG' => 'ADMIN_AUTH_GROUP_TAG',

        'CACHE_AUTH_MENU_WITH_UID' => 'ADMIN_AUTH_MENU_WITH_UID',
        'CACHE_AUTH_MENU_TAG' => 'ADMIN_AUTH_MENU_TAG',

        'CACHE_AUTH_LIST_WITH_UID' => 'ADMIN_AUTH_LIST_WITH_UID',
        'CACHE_AUTH_LIST_TAG' => 'ADMIN_AUTH_LIST_TAG',

        'CACHE_DATA_SCOPE_WITH_UID' => 'ADMIN_DATA_SCOPE_WITH_UID',
        'CACHE_DATA_SCOPE_TAG' => 'ADMIN_DATA_SCOPE_TAG',
    );

    public function __construct(array $config = [])
    {
        $this->_config = array_merge($this->_config, $config);
    }

    /**
     * 检查权限
     * @param  string|array name 需要验证的规则列表,支持逗号分隔的权限规则或索引数组
     * @param  int uid          认证用户的id
     * @param  string mode        执行check的模式
     * @param  string relation   如果为 'or' 表示满足任一条规则即通过验证;如果为 'and'则表示需满足所有规则才能通过验证
     * @return boolean           通过验证返回true;失败返回false
     */
    public function check($name, $uid, $type = 1, $mode = 'url', $relation = 'or')
    {
        if (!$this->_config['AUTH_ON']) {
            return true;
        }

        if (is_super_admin()) {
            return true;
        }

        $authList = $this->getAuthList($uid, $type); //获取用户需要验证的所有有效规则列表
        if (is_string($name)) {
            $name = strtolower($name);
            if (strpos($name, ',') !== false) {
                $name = explode(',', $name);
            } else {
                $name = array($name);
            }
        }
        $list = array(); //保存验证通过的规则名
        if ('url' == $mode) {
            $REQUEST = unserialize(strtolower(serialize($_REQUEST)));
        }
        foreach ($authList as $auth) {
            $query = preg_replace('/^.+\?/U', '', $auth);
            if ('url' == $mode && $query != $auth) {
                parse_str($query, $param); //解析规则中的param
                $intersect = array_intersect_assoc($REQUEST, $param);
                $auth = preg_replace('/\?.*$/U', '', $auth);
                if (in_array($auth, $name) && $intersect == $param) {
                    //如果节点相符且url参数满足
                    $list[] = $auth;
                }
            } else if (in_array($auth, $name)) {
                $list[] = $auth;
            }
        }
        if ('or' == $relation and !empty($list)) {
            return true;
        }
        $diff = array_diff($name, $list);
        if ('and' == $relation and empty($diff)) {
            return true;
        }
        return false;
    }

    /**
     * 根据用户id获取用户组,返回值为数组
     * @param  int uid   用户id
     * @return array       用户所属的用户组 array(
     *     array('uid'=>'用户id','group_id'=>'用户组id','title'=>'用户组名称','rules'=>'用户组拥有的规则id,多个,号隔开'),
     *     ...)
     */
    public function getGroups(int $uid): array
    {
        static $groups = array();
        if (isset($groups[$uid])) {
            return $groups[$uid];
        }

        $user_groups_db = Db::table($this->_config['AUTH_GROUP_ACCESS'])
            ->alias('a')
            ->where([
                'a.uid' => $uid,
                'g.status' => '1'
            ])
            ->join($this->_config['AUTH_GROUP'] . ' g', 'a.group_id=g.id')
            ->field('a.uid,a.group_id,g.title,g.code,g.rules,g.data_scope');

        if ($this->_config['CACHE_ON'] == true) {
            $user_groups_db->cache(
                $this->_config['CACHE_AUTH_GROUP_WITH_UID'] . $uid,
                $this->_config['CACHE_EXPIRE'],
                $this->_config['CACHE_AUTH_GROUP_TAG']
            );
        }

        $user_groups = $user_groups_db->select()->toArray();
        $groups[$uid] = $user_groups ?: array();
        return $groups[$uid];
    }

    /**
     * 获得权限列表
     * @param integer $uid  用户id
     * @param integer $type
     */
    public function getAuthList($uid, $type = 1)
    {
        static $_authList = array(); //保存用户验证通过的权限列表
        $t = implode(',', (array) $type);
        if (isset($_authList[$uid . $t])) {
            return $_authList[$uid . $t];
        }
        // if (2 == $this->_config['AUTH_TYPE'] && isset($_SESSION['_AUTH_LIST_' . $uid . $t])) {
        //     return $_SESSION['_AUTH_LIST_' . $uid . $t];
        // }

        $ids = $this->getRuleIds($uid); //获取用户所属用户组设置的所有权限规则id
        if (empty($ids)) {
            $_authList[$uid . $t] = array();
            return array();
        }

        $map = [
            ['id', 'in', $ids],
            ['type', '=', $type],
            ['status', '=', 1]
        ];
        //读取用户组所有权限规则
        $rules_db = Db::table($this->_config['AUTH_RULE'])->where($map)->field('name');

        if ($this->_config['CACHE_ON'] == true) {
            $rules_db->cache(
                $this->_config['CACHE_AUTH_LIST_WITH_UID'] . $uid, // 注意这里按用户uid,不够精致(结合$map-md5),故清理缓存也会简单粗暴(有影响就全清)
                $this->_config['CACHE_EXPIRE'],
                $this->_config['CACHE_AUTH_LIST_TAG']
            );
        }

        $rules = $rules_db->select();

        //循环规则，判断结果。
        $authList = array(); //
        foreach ($rules as $rule) {
            //只要存在就记录
            $authList[] = strtolower($rule['name']);
        }
        $_authList[$uid . $t] = $authList;
        // if (2 == $this->_config['AUTH_TYPE']) {
        //     //规则列表结果保存到session
        //     $_SESSION['_AUTH_LIST_' . $uid . $t] = $authList;
        // }
        return array_unique($authList);
    }

    /**
     * 获取权限规则ids
     * @param int $uid
     * @return array
     */
    public function getRuleIds(int $uid): array
    {
        //读取用户所属用户组
        $groups = $this->getGroups($uid);
        $ids = []; //保存用户所属用户组设置的所有权限规则id
        foreach ($groups as $g) {
            $ids = array_merge($ids, explode(',', trim($g['rules'], ',')));
        }
        return array_unique($ids);
    }


    /**
     * @var object 对象实例
     */
    protected static $instance;

    /**
     * 初始化
     * @access public
     * @param array $options 参数
     * @return Auth
     */
    public static function instance($options = [])
    {
        if (is_null(self::$instance)) {
            self::$instance = new static($options);
        }
        return self::$instance;
    }


    /**
     * 设置JWT
     */
    public static function encodeJWT(array $value, int $keep_time = 7200): array
    {
        $time = time();
        $payload = [
            // "iss"=>'',           //签发者 可以为空
            // "aud"=>'',           //面象的用户，可以为空  
            'iat' => $time,         //签发时间
            'nbf' => $time,         //在什么时候jwt开始生效  （这里表示生成100秒后才生效）
            'exp' => $time + $keep_time,  //token 过期时间
            'data' => $value
        ];
        $key = env('jwt_secret', 'zhtp666');
        $token = \Firebase\JWT\JWT::encode($payload, $key, 'HS256');
        return [
            'token' => $token,
            'exp' => $payload['exp'],
            'iat' => $payload['iat'],
            'keep_time' => $keep_time,
        ];
    }
    /**
     * 解码JWT
     */
    public static function decodeJWT($jwt)
    {
        if (empty($jwt)) {
            return std_result(-1, 'JWT_token_empty');
        } else {
            try {
                $key = env('jwt_secret', 'zhtp666');
                \Firebase\JWT\JWT::$leeway = 30;
                $decoded = \Firebase\JWT\JWT::decode($jwt, new \Firebase\JWT\Key($key, 'HS256'));
                $decoded_array = json_decode(json_encode($decoded), true);
                return std_result(200, 'ok', $decoded_array);
            } catch (\Exception $e) {
                return std_result(-1, 'JWT_' . $e->getMessage());
            }
        }
    }
    /**
     * 获得Authorization头部信息
     */
    public static function getAuthHeader($request)
    {
        $auth_header = $request->header('Authorization');
        if ($auth_header && str_starts_with($auth_header, 'Bearer ')) {
            $auth_header = substr($auth_header, 7); // 移除"Bearer "前缀
        }
        return $auth_header;
    }

    /**
     * 获得权限+菜单
     * @param int $uid 用户id
     * @return array
     */
    public function getAuthMenus(int $uid): array
    {
        $where = [
            ['status', '=', 1]
        ];

        if (!is_super_admin($uid)) {
            $ids = $this->getRuleIds($uid);
            if (empty($ids)) {
                return [];
            }
            $where[] = ['id', 'in', $ids];
        }

        $rules_db = Db::table($this->_config['AUTH_RULE'])
            ->where($where)
            ->order('rank desc,id asc');

        if ($this->_config['CACHE_ON'] == true) {
            $rules_db->cache(
                $this->_config['CACHE_AUTH_MENU_WITH_UID'] . $uid, // 注意这里按用户uid,不够精致(结合$where-md5),故清理缓存也会简单粗暴(有影响就全清)
                $this->_config['CACHE_EXPIRE'],
                $this->_config['CACHE_AUTH_MENU_TAG']
            );
        }
        $rules = $rules_db->select();

        return $rules->toArray();
    }

    /**
     * 清理缓存
     * @param int|null $uid 用户ID，如果为null则根据tags参数清理缓存
     * @param array $clearWhich 要清理的缓存标志数组，默认为空数组  
     * AUTH_GROUP , AUTH_MENU , AUTH_LIST , DATA_SCOPE
     * @param array $tags 要清理的缓存标签数组，默认为空数组  
     * CACHE_AUTH_GROUP_TAG , CACHE_AUTH_MENU_TAG , CACHE_AUTH_LIST_TAG
     */
    public function clearCache(int $uid = null, array $clearWhich = [], array $tags = [])
    {
        if ($this->_config['CACHE_ON'] == true) {
            if ($uid) {
                // 清理个人缓存
                if (empty($clearWhich)) {
                    $clearWhich = [
                        'AUTH_GROUP',
                        'AUTH_MENU',
                        'AUTH_LIST',
                        'DATA_SCOPE'
                    ];
                }

                $cacheMap = [
                    'AUTH_GROUP' => $this->_config['CACHE_AUTH_GROUP_WITH_UID'],
                    'AUTH_MENU' => $this->_config['CACHE_AUTH_MENU_WITH_UID'],
                    'AUTH_LIST' => $this->_config['CACHE_AUTH_LIST_WITH_UID'],
                    'DATA_SCOPE' => $this->_config['CACHE_DATA_SCOPE_WITH_UID'],
                ];

                foreach ($clearWhich as $key) {
                    if (isset($cacheMap[$key])) {
                        Cache::delete($cacheMap[$key] . $uid);
                    }
                }
            } else {
                // 根据传入的标签清理缓存
                foreach ($tags as $tag) {
                    if (isset($this->_config[$tag])) {
                        Cache::tag($this->_config[$tag])->clear();
                    }
                }
            }
        }
    }

    public const DATA_SCOPE = [
        'NULL' => [0, '未设置'],
        'NONE' => [1, '无权限'],
        'SELF' => [2, '仅本人'],
        'DEPTSELF' => [3, '本部门'],
        'DEPT' => [4, '本部门及以下'],
        'CUSTOM' => [5, '自定义'],
        'ALL' => [6, '全部'],
    ];

    /**
     * 根据用户 ID 获取数据权限范围
     * @param int $uid 用户 ID
     * @return array 合并后的数据权限范围
     */
    public function getDataScope(int $uid): array
    {
        if ($this->_config['CACHE_ON'] == true) {
            $cacheKey = $this->_config['CACHE_DATA_SCOPE_WITH_UID'] . $uid;
            $cachedData = Cache::get($cacheKey);
            if ($cachedData) {
                return $cachedData;
            }
        }

        // 获取用户所属的用户组
        $groups = $this->getGroups($uid); // 依赖getGroups,那么角色变化，上面的缓存也应被清理
        $dataScopes = [];

        // 遍历用户组，获取每个组的数据权限范围
        foreach ($groups as $group) {
            if (isset($group['data_scope'])) {
                $dataScopes[] = $group['data_scope'];
            }
        }

        // 合并、组装数据权限范围
        $dataScope = $this->mergeDataScopes($dataScopes, $uid);

        if ($this->_config['CACHE_ON'] == true) {
            Cache::tag($this->_config['CACHE_DATA_SCOPE_TAG'])
                ->set(
                    $cacheKey,
                    $dataScope,
                    $this->_config['CACHE_EXPIRE']
                );
        }

        return $dataScope;
    }

    /**
     * 检查用户是否有指定的数据权限类型
     * @param int $uid 用户ID 
     * @param int $scopeType 权限类型,使用 DATA_SCOPE 常量定义的类型
     * @return bool 是否有权限
     */
    public function hasDataScope(int $uid, int $scopeType): bool
    {
        $scope = $this->getDataScope($uid);

        // 如果有全部数据权限,直接返回true
        if ($scope[self::DATA_SCOPE['ALL'][0]]) {
            return true;
        }

        // 检查指定类型的权限是否存在数据
        switch ($scopeType) {
            case self::DATA_SCOPE['CUSTOM'][0]:
                return !empty($scope[self::DATA_SCOPE['CUSTOM'][0]]);
            case self::DATA_SCOPE['DEPT'][0]:
                return !empty($scope[self::DATA_SCOPE['DEPT'][0]]);
            case self::DATA_SCOPE['DEPTSELF'][0]:
                return !empty($scope[self::DATA_SCOPE['DEPTSELF'][0]]);
            case self::DATA_SCOPE['SELF'][0]:
                return !empty($scope[self::DATA_SCOPE['SELF'][0]]);
            // case self::DATA_SCOPE['NONE'][0]:
            //     return $scope[self::DATA_SCOPE['NONE'][0]] == true; // 这里是没有数据权限的情况
            // case self::DATA_SCOPE['NULL'][0]:
            //     return true; // 没有设置数据权限的情况
            default:
                return false;
        }
    }

    /**
     * 合并数据权限范围
     * @param array $scopes 数据权限范围数组
     * @return array 合并后的数据权限范围
     */
    private function mergeDataScopes(array $scopes, int $uid): array
    {
        // 初始化数据权限范围数组
        $dataScope = [
            self::DATA_SCOPE['NONE'][0] => 0,
            self::DATA_SCOPE['SELF'][0] => 0,
            self::DATA_SCOPE['DEPT'][0] => [],
            self::DATA_SCOPE['DEPTSELF'][0] => 0,
            self::DATA_SCOPE['CUSTOM'][0] => [],
            self::DATA_SCOPE['ALL'][0] => false,
        ];

        foreach ($scopes as $scope) {
            $scope = json_decode($scope, true);

            if ($scope['type'] === self::DATA_SCOPE['ALL'][0]) {
                $dataScope[self::DATA_SCOPE['ALL'][0]] = true;
            } else if ($scope['type'] === self::DATA_SCOPE['DEPTSELF'][0]) {
                $dept = $this->getDeptByUid($uid);
                $dataScope[self::DATA_SCOPE['DEPTSELF'][0]] = $dept ? $dept['id'] : 0;
            } else if ($scope['type'] === self::DATA_SCOPE['DEPT'][0]) {
                $dataScope[self::DATA_SCOPE['DEPT'][0]] = array_unique(array_merge(
                    $dataScope[self::DATA_SCOPE['DEPT'][0]],
                    $this->getSubDeptIds($uid, true)
                ));
            } elseif ($scope['type'] === self::DATA_SCOPE['CUSTOM'][0]) {
                $dataScope[self::DATA_SCOPE['CUSTOM'][0]] = array_unique(array_merge(
                    $dataScope[self::DATA_SCOPE['CUSTOM'][0]],
                    $scope['custom'] // 自定义部门 ID 数组
                ));
            } elseif ($scope['type'] === self::DATA_SCOPE['SELF'][0]) {
                $dataScope[self::DATA_SCOPE['SELF'][0]] = $uid;
            } elseif ($scope['type'] === self::DATA_SCOPE['NONE'][0]) {
                $dataScope[self::DATA_SCOPE['SELF'][0]] = -99;
            }
            // elseif ($scope['type'] === self::DATA_SCOPE['NULL'][0]) {
            //     // do nothing 不设置的
            // }
        }

        return $dataScope;
    }

    /**
     * 根据部门ID获取其所有子部门的ID
     * @param int $deptId 部门ID
     * @param bool $with_cur_id 是否包含当前部门ID
     * @return array 子部门ID数组
     */
    public function getSubDeptIds(int $uid, bool $with_cur_id = false): array
    {
        $dept = $this->getDeptByUid($uid);
        if (!$dept) {
            return [];
        }

        $subDeptIds = [];
        if ($with_cur_id) {
            $subDeptIds[] = $dept['id'];
        }
        $deptTable = $this->_config['AUTH_DEPT'];

        $oldPath = $dept['parent_ids'] ? $dept['parent_ids'] . ',' . $dept['id'] : $dept['id'];
        $depts = Db::table($deptTable)
            ->where('parent_ids', 'like', $oldPath . '%')
            // ->cache(
            //     'SUB_DEPT_IDS_BY_UID' . $uid,
            //     $this->_config['CACHE_EXPIRE'] // 本方法暂只为getDataScope(已缓存)服务,后面别处复用再缓存优化
            //     // tag
            // )
            ->select();

        foreach ($depts as $dept) {
            $subDeptIds[] = $dept['id'];
        }

        return $subDeptIds;
    }

    public function getDeptByUid(int $uid)
    {
        $user = Db::table($this->_config['AUTH_USER'])
            ->where('id', $uid)
            // ->cache(
            //     'USER' . $uid, // 缓存了，就定义个专用key，同时注意清理的必要时机点
            //     $this->_config['CACHE_EXPIRE'] // 本方法暂只为getDataScope(已缓存)服务,后面别处复用再缓存优化
            //     // tag
            // )
            ->find();
        if (empty($user)) {
            return null;
        }

        $dept = Db::table($this->_config['AUTH_DEPT'])
            ->where('id', $user['dept_id'])
            // ->cache(
            //     'USER_DEPT_WITH_UID' . $uid,
            //     $this->_config['CACHE_EXPIRE'] // 同上
            //     // tag
            // )
            ->find();
        if (empty($dept)) {
            return null;
        }

        return $dept;
    }
}
